<?php
namespace Martdb\Utils;

use Martdb\Enums\Types;
use Martdb\Libs\ThinUtil;
use Martdb\Exceptions\TIllegalArgumentException;

/**
 * ==================================================
 * The Class is Output stream
 * ==================================================
 */
class CodedOutputStream
{
    private $buffer = array();
    private $limit = 0;
    private $position = 0;
    private $output = array();
    
    public const DEFAULT_BUFFER_SIZE = 4096;
    
    public function __construct($bufferSize = self::DEFAULT_BUFFER_SIZE)
    {
        if ($bufferSize < 1)
        {
            $bufferSize = DEFAULT_BUFFER_SIZE;
        }
        $this->limit = $bufferSize;
    }
    
    /** Write a single byte. */
    public function writeRawByte($value) {
        if ($this->position == $this->limit) {
            $this->refreshBuffer();
        }
        $value &= 0xff;
        $this->buffer[$this->position++] = ($value > 127 ? $value - 256 : $value);
    }
    
    /** Write an array of bytes. */
    public function writeRawBytes($value, $offset = 0, $length = -1) {
        // PHP has no way to overload
        if ($length < 0) {
            $length = count($value);
        }
        if ($this->limit - $this->position >= $length) {
            ThinUtil::arraycopy($value, $offset, $this->buffer, $this->position, $length);
            $this->position += $length;
        } else {
            $bytesWritten = $this->limit - $this->position;
            if ($bytesWritten > 0) {
                ThinUtil::arraycopy($value, $offset, $this->buffer, $this->position, $bytesWritten);
                $offset += $bytesWritten;
                $length -= $bytesWritten;
                $this->position = $this->limit;
            }
            $this->refreshBuffer();
            if ($length <= $this->limit) {
                ThinUtil::arraycopy($value, $offset, $this->buffer, 0, $length);
                $this->position = $length;
            } else {
                // Write is very big.  Let's do it all at once.
                ThinUtil::arraycopy($value, $offset, $this->output, count($this->output), $length);
            }
        }
    }
    
    /** Write a {@code bool} field, including tag, to the stream. */
    public function writeBool($value) {
        $this->writeRawByte($value ? Types::BOOLEAN_TRUE : Types::BOOLEAN_FALSE);
    }
    
    /** Write a {@code i32/int} field, including tag, to the stream. */
    public function writeI32($value) {
        $isNeg = $value < 0;
        if ($isNeg) {
            $value = abs(++$value);  // Math methods is inner function
        }
        $size = $this->computeNumberSize($value);
        $this->writeRawByte(Types::I32 | ($isNeg ? 1 << 3 : 0) | ($size - 1));
        $this->putNumber($value, $size);
    }
    
    /** Write a {@code i64/long} field, including tag, to the stream. */
    public function writeI64($value) {
        $isNeg = ($value < 0);
        if ($isNeg) {
            if ($value == PHP_INT_MIN) {
                $value = PHP_INT_MAX;
            } else {
                $value = abs(++$value);
            }
        }
        $size = $this->computeNumberSize($value);
        $this->writeRawByte(Types::I64 | ($isNeg ? 8 : 0) | ($size - 1));
        $this->putNumber($value, $size);
    }
    
    /** Write a {@code float} field, including tag, to the stream. */
    public function writeFloat($value) {
        $this->writeRawByte(Types::FLOAT); // fixed32
        $this->writeRawLittleEndian32($this->floatToRawIntBits($value));
    }
    
    /** Write a {@code double} field, including tag, to the stream. */
    public function writeDouble($value) {
        $this->writeRawByte(Types::DOUBLE); // fixed64
        $this->writeRawLittleEndian64($this->doubleToRawIntBits($value));
    }
    
    /** Write a {@code string} field, including tag, to the stream. */
    public function writeString($value) {
        $length = strlen($value);
        if ($length == 0) {
            $this->writeRawByte(Types::STRING);
            return;
        }
        
        // must be signed char UTF-8
        $bytes = unpack("c*", $value);  // Note: PHP unpack array index start is 1 not 0
        $bytesLength = count($bytes);
        $size = $this->computeNumberSize($bytesLength);
        $this->writeRawByte(Types::STRING | $size);
        $this->putNumber($bytesLength, $size);
        $this->writeRawBytes($bytes, 1, $bytesLength);  // index start is 1
    }
    
    /** Write a {@code byte array} field, including tag, to the stream. */
    public function writeBytes($value, $offset = 0, $length = -1) {
        if ($length < 0) {
            $length = count($value);
        }
        if ($length == 0) {
            $this->writeRawByte(Types::BYTES);
            return;
        }
        $size = $this->computeNumberSize($length);
        $this->writeRawByte(Types::BYTES | $size);
        $this->putNumber($length, $size);
        $this->writeRawBytes($value, $offset, $length);
    }
    
    /** Write a {@code collection} field, including tag, to the stream. */
    public function writeCollection($value, $offset = 0, $length = -1) {
        if ($length < 0) {
            $length = $value->size();
        }
        if ($length == 0) {
            $this->writeRawByte(Types::LIST);
            return;
        }
        $size = $this->computeNumberSize($length);
        $this->writeRawByte(Types::LIST | $size);
        $this->putNumber($length, $size);
        $this->putCollection($value, $offset, $length);
    }
    
    /** Write a {@code map} field, including tag, to the stream. */
    public function writeMap($value) {
        // PHP map is array(key=>value, ...)
        $length = $value->size();
        if ($length == 0) {
            $this->writeRawByte(Types::MAP);
            return;
        }
        $size = $this->computeNumberSize($length);
        $this->writeRawByte(Types::MAP | $size);
        $this->putNumber($length, $size);
        $this->putMap($value);
    }
    
    /** Write a {@code NULL} field, including tag, to the stream. */
    public function writeNull() {
        $this->writeRawByte(Types::NULL);
    }
    
    /** Object != null -> toString() -> getBytes("UTF-8") */
    public function putCollection($value, $offset, $length) {
        for ($i = $offset, $j = $offset + $length; $i < $j; $i++) {
            $this->putObject($value->get($i));
        }
    }
    
    /** Key:Value */
    public function putMap($m) {
        $entrySet = $m->entrySet();
        foreach ($entrySet as $key => $value) {
            $this->putObject($key);
            $this->putObject($value);
        }
    }
    
    /**
     * PHP data type:
     *   Integer
     *   Float
     *   String
     *   Boolean
     *   Array
     *   Object
     */
    public function putObject($value) {
        if (is_string($value)) {
            $this->writeString($value);
        } else if (is_numeric($value)) { // Note: is_numeric check number and string number
            if (is_int($value)) {
                if (-2147483648 <= $value && $value <= 2147483647) {
                    $this->writeI32($value);
                } else {
                    $this->writeI64($value);
                }
            } else {  // is_float(): float->double
                $this->writeDouble($value);
            }
        } else if ($value instanceof TNumber) {
            
            switch ($value->getType()) {
                case TNumber::INT: {
                    $this->writeI32($value->getNumber());
                    break;
                }
                case TNumber::LONG: {
                    $this->writeI64($value->getNumber());
                    break;
                }
                case TNumber::FLOAT: {
                    $this->writeFloat($value->getNumber());
                    break;
                }
                case TNumber::DOUBLE: {
                    $this->writeDouble($value->getNumber());
                    break;
                }
                default: throw new TIllegalArgumentException("Parameter type must be number - ".$value);
            }
            
        } else if ($value instanceof TList) { // list
            $this->writeCollection($value);
        } else if ($value instanceof TMap) {  // map
            $this->writeMap($value);
        } else if (is_array($value)) { // bytes
            $this->writeBytes($value);
        } else if (is_bool($value)) { // boolean
            $this->writeBool($value);
        } else {
            if (is_null($value)) {  // null
                $this->writeNull();
            } else {
                $this->writeString(''.$value);  // __toString()
            }
        }
    }
    
    // this.writeRawLittleEndian32(Float.floatToRawIntBits(value));
    public function writeRawLittleEndian32($value) {
        for ($i = 0; $i < 4; $i++) {
            $this->writeRawByte($value & 0xFF);
            $value >>= 8;
        }
    }
    
    // float32 convert int32 by IEEE 754
    public function floatToRawIntBits($value) {
        return unpack('l*', pack('f', $value))[1];
    }
    
    // this.writeRawLittleEndian64(Double.doubleToRawLongBits(value));
    public function writeRawLittleEndian64($value) {
        for ($i = 0; $i < 8; $i++) {
            $this->writeRawByte($value & 0xFF);
            $value >>= 8;
        }
    }
    
    // float64 convert int64 by IEEE 754
    public function doubleToRawIntBits($value) {
        $a = unpack('L*', pack('d', $value));
        return ThinUtil::isLittleEndian() ? ($a[2] << 32) | $a[1] : ($a[1] << 32) | $a[2];
    }
    
    /**
     * little-endian.
     * PHP unsigned is not supported
     * Support PHP_INT_MAX <= value <= PHP_INT_MIN, 64bit os == java Long.MAX_VALUE/MIN_VALUE,
     * other may be loss of precision
     */
    private function putNumber($value, $size) {
        // $value >>>= 8;  // PHP is not supported >>>, >>>=
        for ($i = 0; $i < $size; $i++) {
            $this->writeRawByte($value);
            $value >>= 8;
        }
    }
    
    /** Compute number size. */
    private function computeNumberSize($value) {
        if ($value > 0) {
            if ($value <= 127)                return 1;
            if ($value <= 32767)              return 2;
            if ($value <= 8388607)            return 3;
            if ($value <= 2147483647)         return 4;
            if ($value <= 549755813887)       return 5;
            if ($value <= 140737488355327)    return 6;
            if ($value <= 36028797018963967)  return 7;
        } else {
            if (-128 <= $value)               return 1;
            if (-32768 <= $value)             return 2;
            if (-8388608 <= $value)           return 3;
            if (-2147483648 <= $value)        return 4;
            if (-549755813888 <= $value)      return 5;
            if (-140737488355328 <= $value)   return 6;
            if (-36028797018963968 <= $value) return 7;
        }
        return 8;
    }
    
    public function refreshBuffer() {
        if ($this->position == 0) {
            return;
        }
        // Since we have an output stream, this is our buffer
        // and buffer offset == 0
        ThinUtil::arraycopy($this->buffer, 0, $this->output, count($this->output), $this->position);
        $this->position = 0;
    }
    
    /**
     * Flushes the stream and forces any buffered bytes to be written.  This
     * does not flush the underlying OutputStream.
     */
    public function flush() {
        if (!is_null($this->output)) {
            $this->refreshBuffer();
        }
    }
    
    // Output unsigned byte array() string(Thrift binary type)
    public function output() {
        $this->flush();
        return ThinUtil::array2String($this->output);
    }
    
    public function getOutput() {
        return $this->output;
    }
    
}

